package parse;

import query.Constant;
import query.Expression;
import query.Predicate;
import query.Term;
import record.Schema;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Supplier;

/**
 * The SimpleDB parser.
 * @author Edward Sciore
 */
public class Parser {
    private Lexer lex;

    public Parser(String s) {
        lex = new Lexer(s);
    }

    // Methods for parsing predicates, terms, expressions, constants, and fields

    public String field() {
        return lex.eatId();
    }

    public Constant constant() {
        if (lex.matchStringConstant())
            return new Constant(lex.eatStringConstant());
        else
            return new Constant(lex.eatIntConstant());
    }

    // <Expression> := <Field> | <Constant>
    public Expression expression() {
        if (lex.matchId())
            return new Expression(field());
        else
            return new Expression(constant());
    }

    // <Term> := <Expression> = <Expression
    public Term term() {
        Expression lhs = expression();
        lex.eatDelim('=');
        Expression rhs = expression();
        return new Term(lhs, rhs);
    }

    // <Predicate> := <Term> [ AND <Predicate> ]
    public Predicate predicate() {
        Predicate pred = new Predicate(term());
        if (lex.matchKeyword("and")) {
            lex.eatKeyword("and");
            pred.conjoinWith(predicate());
        }
        return pred;
    }

    // Methods for parsing queries

    // <Query> := SELECT <SelectList> FROM <TableList> [ WHERE <Predicate> ]
    public QueryData query() {
        lex.eatKeyword("select");
        List<String> fields = fieldList();
        lex.eatKeyword("from");
        Collection<String> tables = tableList();
        Predicate pred = new Predicate();
        if (lex.matchKeyword("where")) {
            lex.eatKeyword("where");
            pred = predicate();
        }
        return new QueryData(fields, tables, pred);
    }

    // <SelectList> := <Field> [ , <SelectList>  ]
    private List<String> fieldList() {
        return parseList(this::field);
    }

    // <ConstList> := <Constant> [ , <ConstList> ]
    private List<Constant> constList() {
        return parseList(this::constant);
    }

    // <TableList> := IdTok [ , <TableList>    ]
    private List<String> tableList() {
        return parseList(this::field);
    }

    // <FieldDefs> := <FieldDef> [ , <FieldDefs> ]
    private Schema fieldDefs() {
        Schema schema = fieldDef();
        if (lex.matchDelim(',')) {
            lex.eatDelim(',');
            Schema schema2 = fieldDefs();
            schema.addAll(schema2);
        }
        return schema;
    }


    private <T> List<T> parseList(Supplier<T> func) {
        List<T> result = new ArrayList<>();

        // 用循環代替遞歸
        while (true) {
            result.add(func.get());
            if (lex.matchDelim(',')) {
                lex.eatDelim(',');
            } else {
                break;
            }
        }

        return result;
    }

    // <FieldDef> := IdTok <TypeDef>
    private Schema fieldDef() {
        String fldname = field();
        return fieldType(fldname);
    }

    // <TypeDef> := INT | VARCHAR ( IntTok )
    private Schema fieldType(String fldname) {
        Schema schema = new Schema();
        if (lex.matchKeyword("int")) {
            lex.eatKeyword("int");
            schema.addIntField(fldname);
        } else {
            lex.eatKeyword("varchar");
            lex.eatDelim('(');
            int strLen = lex.eatIntConstant();
            lex.eatDelim(')');
            schema.addStringField(fldname, strLen);
        }

        return schema;
    }

    // Methods for parsing the various update commands

    // <Insert> | <Delete> | <Modify> | <Create>
    public Object updateCmd() {
        if (lex.matchKeyword("insert"))
            return insert();
        else if (lex.matchKeyword("delete"))
            return delete();
        else if (lex.matchKeyword("update"))
            return modify();
        else
            return create();
    }

    // <Create> := <CreateTable> | <CreateView> | <CreateIndex>
    private Object create() {
        lex.eatKeyword("create");
        if (lex.matchKeyword("table"))
            return createTable();
        else if (lex.matchKeyword("view"))
            return createView();
        else
            return createIndex();
    }

    // Method for parsing delete commands

    // <Delete> := DELETE FROM IdTok [ WHERE <Predicate> ]
    public DeleteData delete() {
        lex.eatKeyword("delete");
        lex.eatKeyword("from");
        String tblname = lex.eatId();
        Predicate pred = new Predicate();
        if (lex.matchKeyword("where")) {
            lex.eatKeyword("where");
            pred = predicate();
        }

        return new DeleteData(tblname, pred);
    }

    // Methods for parsing insert commands

    // <Insert> := INSERT INTO IdTok ( <FieldList> ) VALUES ( <ConstList> )
    public InsertData insert() {
        lex.eatKeyword("insert");
        lex.eatKeyword("into");
        String tblname = lex.eatId();

        lex.eatDelim('(');
        List<String> flds = fieldList();
        lex.eatDelim(')');

        lex.eatKeyword("values");
        lex.eatDelim('(');
        List<Constant> vals = constList();
        lex.eatDelim(')');

        return new InsertData(tblname, flds, vals);
    }

    // Method for parsing modify commands
    // <Modify> := UPDATE IdTok SET <Field> = <Expression> [ WHERE <Predicate> ]
    public ModifyData modify() {
        lex.eatKeyword("update");
        String tblname = lex.eatId();

        lex.eatKeyword("set");
        String fldname = field();
        lex.eatDelim('=');
        Expression newval = expression();

        Predicate pred = new Predicate();
        if (lex.matchKeyword("where")) {
            lex.eatKeyword("where");
            pred = predicate();
        }
        return new ModifyData(tblname, fldname, newval, pred);
    }

    // Method for parsing create table commands

    // <CreateTable> := CREATE TABLE IdTok ( <FieldDefs> )
    public CreateTableData createTable() {
        lex.eatKeyword("table");
        String tblname = lex.eatId();
        lex.eatDelim('(');
        Schema sch = fieldDefs();
        lex.eatDelim(')');
        return new CreateTableData(tblname, sch);
    }

    // Method for parsing create view commands

    // <CreateView> := CREATE VIEW IdTok AS <Query>
    public CreateViewData createView() {
        lex.eatKeyword("view");
        String viewname = lex.eatId();
        lex.eatKeyword("as");
        QueryData qd = query();
        return new CreateViewData(viewname, qd);
    }

    //  Method for parsing create index commands
    // <CreateIndex> := CREATE INDEX IdTok ON IdTok ( <Field> )
    public CreateIndexData createIndex() {
        lex.eatKeyword("index");
        String idxname = lex.eatId();

        lex.eatKeyword("on");
        String tblname = lex.eatId();

        lex.eatDelim('(');
        String fldname = field();
        lex.eatDelim(')');

        return new CreateIndexData(idxname, tblname, fldname);
    }
}
